/*
 * Copyright 2002-2006 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language governing permissions and limitations under the
 * License.
 */
package pikes.xml;

import java.util.HashMap;
import java.util.Map;

import pikes.core.Printable;
import pikes.core.Printer;
import pikes.util.Assert;

/**
 * XML printer which prints the content to a character stream without formatting. A boolean parameter can be passed to
 * the constructor, specifying if the empty tags should be printed with an opening and closing tags. The default is
 * false.
 * @author Peter Bona
 */

public class XMLWriter implements XMLVisitor {

	private static Map<Character, String> ENTITY_REFERENCES = new HashMap<Character, String>();

	static {
		ENTITY_REFERENCES.put('<', "&lt;");
		ENTITY_REFERENCES.put('>', "&gt;");
		ENTITY_REFERENCES.put('&', "&amp;");
		ENTITY_REFERENCES.put('\'', "&apos;");
		ENTITY_REFERENCES.put('\"', "&quot;");
	}

	private boolean shouldExpandEmptyTag = false;

	private Printer printer = null;

	/**
	 * Creates a XML writer
	 * @param appendable Character stream where the XML content is written to.
	 */
	public XMLWriter(final Appendable appendable) {
		this(new Printer(appendable));
	}

	public XMLWriter(final Printer printer) {
		this.printer = printer;
	}

	/**
	 * Creates a XML writer
	 * @param appendable Character stream where the XML content is written to.
	 * @param shouldExpandEmptyTag If true, the empty tags are printed as opening and closing tags
	 */
	public XMLWriter(final Appendable appendable, final boolean shouldExpandEmptyTag) {
		this(new Printer(appendable), shouldExpandEmptyTag);
	}

	public XMLWriter(final Printer printer, final boolean shouldExpandEmptyTag) {
		this.printer = printer;
		this.shouldExpandEmptyTag = shouldExpandEmptyTag;
	}

	/**
	 * Prints the beginning of an XML tag, e.g.: <code>
	 * &lt;name
	 * </code>
	 * @param name Name of the tag
	 * @throws XMLVisitingException if an IOException is thrown on the underlying Appendable
	 */
	public final void openingFirstTag(CharSequence name) throws XMLVisitingException {
		Assert.hasText(name, "Tag name is empty.");
		printer.print('<');
		printer.print(name);
	}

	/**
	 * Prints the beginning of an XML tag, e.g.: <code>
	 * &lt;prefix:name
	 * </code>
	 * @param prefix Prefix of the tag
	 * @param name Name of the tag
	 * @throws XMLVisitingException if an IOException is thrown on the underlying Appendable
	 */
	public final void openingFirstTag(CharSequence prefix, CharSequence name) throws XMLVisitingException {
		Assert.hasText(prefix, "Tag prefix is empty.");
		Assert.hasText(name, "Tag name is empty.");
		printer.print('<');
		printer.print(prefix);
		printer.print(':');
		printer.print(name);
	}

	/**
	 * Prints the end of the first XML tag, <code>/&gt;</code> or <code>&gt;</code> depending on the parameter.
	 * @param name Name of the tag
	 * @param hasChildren if true, <code>/&gt;</code> will be printed, otherwise <code>&gt;</code> will be printed
	 * @throws XMLVisitingException if an IOException is thrown on the underlying Appendable
	 */
	public final void closingFirstTag(CharSequence name, boolean hasChildren) throws XMLVisitingException {
		Assert.hasText(name, "Tag name is empty.");
		if (hasChildren) {
			printer.print('>');
		}
		else {
			if (shouldExpandEmptyTag) {
				printer.print('>');
				closeSecondTag(name);
			}
			else {
				printer.print('/');
				printer.print('>');
			}
		}
	}

	/**
	 * Prints the end of the second XML tag, <code>&lt;/name&gt;</code>
	 * @param name Name of the tag
	 * @throws XMLVisitingException if an IOException is thrown on the underlying Appendable
	 */
	public final void closeSecondTag(CharSequence name) throws XMLVisitingException {
		Assert.hasText(name, "Tag name is empty.");
		printer.print('<');
		printer.print('/');
		printer.print(name);
		printer.print('>');
	}

	/**
	 * Prints the attribute's name and value: <code> key=&quot;value&quot;</code>
	 * @param name name of the attribute
	 * @param value value of the attribute
	 * @throws XMLVisitingException if an IOException is thrown on the underlying Appendable
	 */
	public final void attribute(CharSequence name, CharSequence value) throws XMLVisitingException {
		Assert.hasText(name);
		Assert.notNull(value);
		printer.print(' ');
		printer.print(name);
		printer.print('=');
		printer.print('\"');
		printer.print(value);
		printer.print('\"');
	}

	public final void attribute(CharSequence name, Printable value) throws XMLVisitingException {
		Assert.hasText(name);
		Assert.notNull(value);
		printer.print(' ');
		printer.print(name);
		printer.print('=');
		printer.print('\"');
		printer.print(value);
		printer.print('\"');
	}

	/**
	 * Prints the character data as it is.
	 * @param text character data
	 * @throws XMLVisitingException if an IOException is thrown on the underlying Appendable
	 */
	public final void text(CharSequence text) throws XMLVisitingException {
		Assert.notNull(text);
		printer.print(text);
	}

	public final void text(Printable printable) throws XMLVisitingException {
		Assert.notNull(printable);
		printer.print(printable);
	}

	/**
	 * Prints the XML declaration, e.g.: <code>
	 * &lt;?xml version=&quot;1.0&quot;?&gt;
	 * </code>
	 * @throws XMLVisitingException if an IOException is thrown on the underlying Appendable
	 */
	public final void xmlDeclaration(CharSequence version) throws XMLVisitingException {
		printer.print("<?xml version=\"");
		printer.print(version);
		printer.print("\"?>");
	}

	/**
	 * Prints the XML declaration, e.g.: <code>
	 * &lt;?xml version=&quot;1.0&quot; encoding=&quot;encoding&quot?&gt;
	 * </code>
	 * @throws XMLVisitingException if an IOException is thrown on the underlying Appendable
	 */
	public final void xmlDeclaration(CharSequence version, CharSequence encoding) throws XMLVisitingException {
		printer.print("<?xml version=\"");
		printer.print(version);
		printer.print("\" ");
		printer.print("encoding=\"");
		printer.print(encoding);
		printer.print("\"?>");
	}

	/**
	 * Prints the PUBLIC doc type declaration, e.g.:
	 * <code>&lt;!DOCTYPE name PUBLIC &quot;id&quot; &quot;url&quot;&gt;</code>
	 * @throws XMLVisitingException if an IOException is thrown on the underlying Appendable
	 */
	public final void publicDocType(CharSequence name, CharSequence id, CharSequence uri) throws XMLVisitingException {
		printer.print("<!DOCTYPE ");
		printer.print(name);
		printer.print(" PUBLIC \"");
		printer.print(id);
		printer.print("\" \"");
		printer.print(uri);
		printer.print("\">");
	}

	/**
	 * Prints the XML comment, e.g.: <code>&lt;!-- comment --&gt;</code>
	 * @throws XMLVisitingException if an IOException is thrown on the underlying Appendable
	 */
	public final void comment(CharSequence comment) throws XMLVisitingException {
		if (comment != null) {
			printer.print("<!-- ");
			printer.print(comment);
			printer.print(" -->");
		}
	}

}
